﻿using System;
using System.ComponentModel;
using System.Diagnostics;

namespace Hardcodet.Wpf.TaskbarNotification.Interop
{
	/// <summary>
	/// Receives messages from the taskbar icon through
	/// window messages of an underlying helper window.
	/// </summary>
	public class WindowMessageSink : IDisposable
	{
		#region members

		/// <summary>
		/// The ID of messages that are received from the the
		/// taskbar icon.
		/// </summary>
		public const int CallbackMessageId = 0x400;

		/// <summary>
		/// Used to track whether a mouse-up event is just
		/// the aftermath of a double-click and therefore needs
		/// to be suppressed.
		/// </summary>
		private bool _isDoubleClick;

		/// <summary>
		/// A delegate that processes messages of the hidden
		/// native window that receives window messages. Storing
		/// this reference makes sure we don't loose our reference
		/// to the message window.
		/// </summary>
		private WindowProcedureHandler _messageHandler;

		/// <summary>
		/// The ID of the message that is being received if the
		/// taskbar is (re)started.
		/// </summary>
		private uint _taskbarRestartMessageId;

		/// <summary>
		/// Window class ID.
		/// </summary>
		internal string WindowId { get; private set; }

		/// <summary>
		/// Handle for the message window.
		/// </summary>
		internal IntPtr MessageWindowHandle { get; private set; }

		/// <summary>
		/// The version of the underlying icon. Defines how
		/// incoming messages are interpreted.
		/// </summary>
		public NotifyIconVersion Version { get; set; }

		#endregion

		#region events

		/// <summary>
		/// The custom tooltip should be closed or hidden.
		/// </summary>
		public event Action<bool> ChangeToolTipStateRequest;

		/// <summary>
		/// Fired in case the user clicked or moved within
		/// the taskbar icon area.
		/// </summary>
		public event Action<MouseEvent> MouseEventReceived;

		/// <summary>
		/// Fired if a balloon ToolTip was either displayed
		/// or closed (indicated by the boolean flag).
		/// </summary>
		public event Action<bool> BalloonToolTipChanged;

		/// <summary>
		/// Fired if the taskbar was created or restarted. Requires the taskbar
		/// icon to be reset.
		/// </summary>
		public event Action TaskbarCreated;

		#endregion

		#region construction

		/// <summary>
		/// Creates a new message sink that receives message from
		/// a given taskbar icon.
		/// </summary>
		/// <param name="version"></param>
		public WindowMessageSink(NotifyIconVersion version)
		{
			this.Version = version;
			this.CreateMessageWindow();
		}

		private WindowMessageSink() {}

		/// <summary>
		/// Creates a dummy instance that provides an empty
		/// pointer rather than a real window handler.<br/>
		/// Used at design time.
		/// </summary>
		/// <returns></returns>
		static internal WindowMessageSink CreateEmpty()
		{
			return new WindowMessageSink {MessageWindowHandle = IntPtr.Zero, Version = NotifyIconVersion.Vista};
		}

		#endregion

		#region CreateMessageWindow

		/// <summary>
		/// Creates the helper message window that is used
		/// to receive messages from the taskbar icon.
		/// </summary>
		private void CreateMessageWindow()
		{
			//generate a unique ID for the window
			this.WindowId = "WPFTaskbarIcon_" + DateTime.Now.Ticks;

			//register window message handler
			this._messageHandler = this.OnWindowMessageReceived;

			// Create a simple window class which is reference through
			//the messageHandler delegate
			WindowClass wc;

			wc.style = 0;
			wc.lpfnWndProc = this._messageHandler;
			wc.cbClsExtra = 0;
			wc.cbWndExtra = 0;
			wc.hInstance = IntPtr.Zero;
			wc.hIcon = IntPtr.Zero;
			wc.hCursor = IntPtr.Zero;
			wc.hbrBackground = IntPtr.Zero;
			wc.lpszMenuName = "";
			wc.lpszClassName = this.WindowId;

			// Register the window class
			WinApi.RegisterClass(ref wc);

			// Get the message used to indicate the taskbar has been restarted
			// This is used to re-add icons when the taskbar restarts
			this._taskbarRestartMessageId = WinApi.RegisterWindowMessage("TaskbarCreated");

			// Create the message window
			this.MessageWindowHandle = WinApi.CreateWindowEx(0, this.WindowId, "", 0, 0, 0, 1, 1, 0, 0, 0, 0);

			if (this.MessageWindowHandle == IntPtr.Zero)
				throw new Win32Exception();
		}

		#endregion

		#region Handle Window Messages

		/// <summary>
		/// Callback method that receives messages from the taskbar area.
		/// </summary>
		private long OnWindowMessageReceived(IntPtr hwnd, uint messageId, uint wparam, uint lparam)
		{
			if (messageId == this._taskbarRestartMessageId)
			{
				//recreate the icon if the taskbar was restarted (e.g. due to Win Explorer shutdown)
				this.TaskbarCreated();
			}

			//forward message
			this.ProcessWindowMessage(messageId, wparam, lparam);

			// Pass the message to the default window procedure
			return WinApi.DefWindowProc(hwnd, messageId, wparam, lparam);
		}

		/// <summary>
		/// Processes incoming system messages.
		/// </summary>
		/// <param name="msg">Callback ID.</param>
		/// <param name="wParam">If the version is <see cref="NotifyIconVersion.Vista"/>
		/// or higher, this parameter can be used to resolve mouse coordinates.
		/// Currently not in use.</param>
		/// <param name="lParam">Provides information about the event.</param>
		private void ProcessWindowMessage(uint msg, uint wParam, uint lParam)
		{
			if (msg != CallbackMessageId)
				return;

			switch (lParam)
			{
				case 0x200:
					this.MouseEventReceived(MouseEvent.MouseMove);
					break;

				case 0x201:
					this.MouseEventReceived(MouseEvent.IconLeftMouseDown);
					break;

				case 0x202:
					if (!this._isDoubleClick)
						this.MouseEventReceived(MouseEvent.IconLeftMouseUp);
					this._isDoubleClick = false;
					break;

				case 0x203:
					this._isDoubleClick = true;
					this.MouseEventReceived(MouseEvent.IconDoubleClick);
					break;

				case 0x204:
					this.MouseEventReceived(MouseEvent.IconRightMouseDown);
					break;

				case 0x205:
					this.MouseEventReceived(MouseEvent.IconRightMouseUp);
					break;

				case 0x206:
					//double click with right mouse button - do not trigger event
					break;

				case 0x207:
					this.MouseEventReceived(MouseEvent.IconMiddleMouseDown);
					break;

				case 520:
					this.MouseEventReceived(MouseEvent.IconMiddleMouseUp);
					break;

				case 0x209:
					//double click with middle mouse button - do not trigger event
					break;

				case 0x402:
					this.BalloonToolTipChanged(true);
					break;

				case 0x403:
				case 0x404:
					this.BalloonToolTipChanged(false);
					break;

				case 0x405:
					this.MouseEventReceived(MouseEvent.BalloonToolTipClicked);
					break;

				case 0x406:
					this.ChangeToolTipStateRequest(true);
					break;

				case 0x407:
					this.ChangeToolTipStateRequest(false);
					break;

				default:
					Debug.WriteLine("Unhandled NotifyIcon message ID: " + lParam);
					break;
			}
		}

		#endregion

		#region Dispose

		/// <summary>
		/// Set to true as soon as <see cref="Dispose"/>
		/// has been invoked.
		/// </summary>
		public bool IsDisposed { get; private set; }

		/// <summary>
		/// Disposes the object.
		/// </summary>
		/// <remarks>This method is not virtual by design. Derived classes
		/// should override <see cref="Dispose(bool)"/>.
		/// </remarks>
		public void Dispose()
		{
			this.Dispose(true);

			// This object will be cleaned up by the Dispose method.
			// Therefore, you should call GC.SupressFinalize to
			// take this object off the finalization queue 
			// and prevent finalization code for this object
			// from executing a second time.
			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// This destructor will run only if the <see cref="Dispose()"/>
		/// method does not get called. This gives this base class the
		/// opportunity to finalize.
		/// <para>
		/// Important: Do not provide destructors in types derived from
		/// this class.
		/// </para>
		/// </summary>
		~WindowMessageSink()
		{
			this.Dispose(false);
		}

		/// <summary>
		/// Removes the windows hook that receives window
		/// messages and closes the underlying helper window.
		/// </summary>
		private void Dispose(bool disposing)
		{
			//don't do anything if the component is already disposed
			if (this.IsDisposed || !disposing)
				return;
			this.IsDisposed = true;

			WinApi.DestroyWindow(this.MessageWindowHandle);
			this._messageHandler = null;
		}

		#endregion
	}
}